package com.gambi.tapestry.services.gwt;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

import org.apache.tapestry5.ComponentResources;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.BeginRender;
import org.apache.tapestry5.json.JSONArray;
import org.apache.tapestry5.json.JSONObject;
import org.apache.tapestry5.model.MutableComponentModel;
import org.apache.tapestry5.services.AssetSource;
import org.apache.tapestry5.services.ClassTransformation;
import org.apache.tapestry5.services.ComponentClassTransformWorker;
import org.apache.tapestry5.services.ComponentMethodAdvice;
import org.apache.tapestry5.services.ComponentMethodInvocation;
import org.apache.tapestry5.services.FieldAccess;
import org.apache.tapestry5.services.TransformConstants;
import org.apache.tapestry5.services.TransformField;
import org.apache.tapestry5.services.TransformMethod;
import org.apache.tapestry5.services.javascript.JavaScriptSupport;
import org.slf4j.Logger;

import com.trsvax.tapestry.gwt.annotation.GWT;

public class GWTWorker implements ComponentClassTransformWorker {
	private final Logger logger;
	private final JavaScriptSupport javaScriptSupport;
	private final AssetSource assetSource;
	private final GWTComponentEventSource gwtComponentEvent;

	public GWTWorker(Logger logger, JavaScriptSupport javaScriptSupport,
			AssetSource assetSource, GWTComponentEventSource gwtComponentEvent) {
		this.logger = logger;
		this.javaScriptSupport = javaScriptSupport;
		this.assetSource = assetSource;
		this.gwtComponentEvent = gwtComponentEvent;
	}

	// Must consider name clash of parameters beloging to different component
	// hosted inside the same component or page
	private Map<String, Map<String, Object>> gwtParameters = new HashMap<String, Map<String, Object>>();

	public void transform(ClassTransformation transformation,
			MutableComponentModel model) {
		System.out.println("GWTWorker.transform() "
				+ transformation.getClassName());
		gwtParameters.put(transformation.getClassName(),
				new HashMap<String, Object>());

		processClassAnnotationAtSetupRenderPhase(transformation, model);

	}

	/*
	 * Get all the parameteres of this component annotated with the GWT
	 * annotation. Note this method consider BaseClass parameters as well
	 */
	private void extractGwtParameter(ClassTransformation transformation,
			MutableComponentModel model) {

		System.out.println("GWTWorker.extractGwtParameter()"
				+ transformation.getClassName());
		System.out
				.println("GWTWorker.extractGwtParameter() Component parameter list"
						+ "" + model.getParameterNames());
		for (String parameterName : model.getParameterNames()) {
			System.out.println(parameterName);
			// Local search, then go up in the Hierarchy of components
			if (transformation.isField(parameterName)) {
				System.out.println("GWTWorker.transform() field of this class "
						+ parameterName);
				TransformField field = transformation.getField(parameterName);
				if (field.getAnnotation(GWT.class) != null) {
					System.out
							.println("GWTWorker.transform() Adding as GWT Parameter "
									+ parameterName);
					gwtParameters.get(transformation.getClassName()).put(
							parameterName, field.getAccess());
				}
			} else {
				System.out
						.println("GWTWorker.transform() Not a field of this class "
								+ parameterName);
			}
		}
	}

	private void processClassAnnotationAtSetupRenderPhase(
			ClassTransformation transformation, MutableComponentModel model) {

		// System.out
		// .println("GWTWorker.processClassAnnotationAtSetupRenderPhase()");
		//
		// System.out.println("Class SUMMARY : ");
		// System.out.println(transformation.getClassName());
		// System.out.println("DONT REMEBER HOW TO GET CLASS FIELD ");
		//
		// System.out.println(" Model SUMMARY: ");
		// System.out.println(model.getComponentClassName());
		// System.out.println(model.getParameterNames());
		// System.out.println(model.getDeclaredParameterNames());
		// System.out.println(model.getPersistentFieldNames());

		// Only components annotated via the @GWT are considered
		GWT annotation = transformation.getAnnotation(GWT.class);
		if (annotation == null) {
			return;
		}
		// Enable the informal parameters
		model.enableSupportsInformalParameters();
		// Advice the BeginRender phase
		model.addRenderPhase(BeginRender.class);
		TransformMethod beginRender = transformation
				.getOrCreateMethod(TransformConstants.BEGIN_RENDER_SIGNATURE);
		adviceBeginMethod(transformation, beginRender, annotation);

		// Extract all the initialization parameters of the component
		extractGwtParameter(transformation, model);
	}

	private void adviceBeginMethod(final ClassTransformation transformation,
			TransformMethod method, final GWT annotation) {

		method.addAdvice(new ComponentMethodAdvice() {
			@Override
			public void advise(ComponentMethodInvocation invocation) {
				ComponentResources componentResources = invocation
						.getComponentResources();

				// Import the js libraries
				importJsLib(annotation);

				// Create the js namespaces
				// Mandatory elements for the rendering: ComponentType,
				// ComponentID
				if (annotation.componentType() != null
						&& !annotation.componentType().trim().equals("")) {

					renderJsNamespace(invocation, annotation,
							gwtParameters.get(transformation.getClassName()),
							(MarkupWriter) invocation.getParameter(0),
							componentResources);
					// Render the DIV tag that will host this component
					render((MarkupWriter) invocation.getParameter(0),
							componentResources);
				}

				// Finish to render the component
				invocation.proceed();
			}
		});
	}

	private void importJsLib(GWT annotation) {
		// Import the tapestry-gwt.js library - TODO Change to something
		// different than this. We need to contribute to some service!!
		javaScriptSupport.importJavaScriptLibrary(assetSource
				.getClasspathAsset(
						"com/gambi/tapestry/tapestry-gwt.js", null));
		javaScriptSupport
				.importJavaScriptLibrary(assetSource
						.getClasspathAsset(
								"com/gambi/tapestry/tapestry-gwt-initializer.js",
								null));

		// This is mandatory to link the GWT Dictionary
		String moduleClass = annotation.entryPoint().getName();
		// This can be overwritten to rename the module nochace.js file
		String moduleName = annotation.entryPointName();
		if (moduleName.trim().equals("")) {
			// By default if the moduleName is not provided with keep the class
			// Name
			moduleName = moduleClass;
		}
		// Add the import statement for the module library
		javaScriptSupport.importJavaScriptLibrary(assetSource.getContextAsset(
				String.format("js/gwtjs/%s/%s.nocache.js", moduleName,
						moduleName), null));
	}

	private void render(MarkupWriter writer,
			ComponentResources componentResources) {
		// Render the DIV element that will host this component

		// TODO Not sure this is the right way to do that...
		ArrayList<Object> informalParameters = new ArrayList<Object>();
		informalParameters.add("id");
		informalParameters.add(componentResources.getId());
		for (String name : componentResources.getInformalParameterNames()) {
			System.out
					.println("GWTWorker.render() informal parameters " + name);
			informalParameters.add(name);
			informalParameters.add(componentResources.getInformalParameter(
					name, String.class));
			// XXX
			System.out
					.println("GWTWorker.render() Add the parameter to GWT Component "
							+ componentResources.getClass());

			gwtParameters.get(componentResources.getClass())
					.put(name,
							componentResources.getInformalParameter(name,
									String.class));
		}

		writer.element("div", informalParameters.toArray());
		writer.end();
	}

	private void renderJsNamespace(ComponentMethodInvocation invocation,
			GWT annotation, Map<String, Object> parameters,
			MarkupWriter writer, ComponentResources componentResources) {

		System.out.println("GWTWorker.renderJsNamespace() with parameters "
				+ parameters);

		// This is mandatory to link the GWT Dictionary - TODO Move this inside
		// parameters
		JSONObject componentInitializer = new JSONObject("moduleName",
				annotation.entryPoint().getName());
		JSONObject component = new JSONObject();
		JSONObject componentParameters = new JSONObject();

		// This is mandatory for rendering
		String componentType = annotation.componentType();

		// Parameter is a JSONObject
		component.put("componentId", componentResources.getId());
		component.put("type", componentType);

		// Add component events if any
		Set<String> events = gwtComponentEvent
				.getEventSetForComponent(componentResources.getComponentModel()
						.getComponentClassName());
		if (events != null) {
			componentParameters.put(
					"eventLink",
					""
							+ componentResources.createEventLink("",
									new Object[] {}));
			JSONArray jsonEvents = new JSONArray();
			for (String event : events) {
				jsonEvents.put(event);
			}

			componentParameters.put("eventList", jsonEvents);
		}

		// Add the component parameters annotated via @GWT if any
		for (String name : parameters.keySet()) {
			// TODO
			// HERE THERE MUST BE SOME KIND OF COERCION FROM VALUE
			// TO SOME BASE TYPE (int, bool, string)
			// OR
			// TO SOME JSON, JSONArray
			// TODO Here some conversion/coercion should take place...
			// otherwise

			// TODO For the moment we use the toString() method
			if (parameters.get(name) instanceof FieldAccess) {
				// Process the Parameter Field
				Object value = ((FieldAccess) parameters.get(name))
						.read(invocation.getInstance());
				if (value != null) {
					componentParameters.put(name, value.toString());
				} else {
					componentParameters.put(name, "");
				}
			} else {
				// Process any informal parameter. Assume that they are just
				// // strings
				componentParameters.put(name, parameters.get(name).toString());
			}
		}

		component.put("componentParameters", componentParameters);
		componentInitializer.put("component", component);
		javaScriptSupport.addInitializerCall("createGwtComponent",
				componentInitializer);
	}
}
